//@author A0099223B

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Parser {

	protected static final String COMMAND_ADD = "add";
	protected static final String COMMAND_EDIT = "edit";
	protected static final String COMMAND_VIEW = "view";
	protected static final String COMMAND_LIST = "list";
	protected static final String COMMAND_DELETE = "delete";
	protected static final String COMMAND_UNDO = "undo";
	protected static final String COMMAND_REDO = "redo";
	protected static final String COMMAND_HELP = "help";
	protected static final String COMMAND_DONE = "done";
	protected static final String COMMAND_NOTDONE = "notdone";
	protected static final String COMMAND_EXIT = "exit";
	protected static final String COMMAND_SEARCHBOX = "searchbox";

	protected static final String STRING_WHITESPACE = " ";

	protected static final String TAG_START_TIME = "/st";
	protected static final String TAG_END_TIME = "/et";
	protected static final String TAG_PLACE = "/pl";
	protected static final String TAG_DESCRIPTION = "/des";
	protected static final String TAG_KEYWORD = "/key";
	protected static final String TAG_ID = "/id";
	protected static final String TAG_TITLE = "/tit";
	protected static final String TAG_DONE = "-d";
	protected static final String TAG_NOTDONE = "-nd";
	protected static final char TAG_PROJECT = '#';

	protected static final String MESSAGE_UNPRECEEDED_BY_TAG = "Attribute tag needed before \"%1$s\"";
	private static final String MESSAGE_MISSING_DETAILS = "Missing Details";
	private static final String MESSAGE_REPEATING_TAGS = "Repeating tags: %1$s";
	private static final String MESSAGE_COMMAND_IS_ONE_WORD = "\"%1$s\" should not be followed by details";
	private static final String MESSAGE_REVERSED_DATES = "Error: start time (%1$s) comes after end time (%2$s)";
	private static final String MESSAGE_MISSING_TITLE_AND_ID = "Please indicate task with id or title.";
	private static final String MESSAGE_USE_TITLE_OR_ID_NOT_BOTH = "Please refer to task by either title or id.";
	private static final String MESSAGE_NO_INPUT = "No input!";

	private static final String MESSAGE_UNKNOWN_COMMAND = "Sorry, we could not understand this command.";
	private static final String MESSAGE_UNUSABLE_TAG = "%1$s tag(s) not available for %2$s";
	private static final String MESSAGE_MISSING_INPUT_AFTER_TAG = "Missing input after %1$s";
	private static final String MESSAGE_INVALID_TASK_ID = "Invalid Task ID: %1$s";

	private static final String DEFAULT_DISPLAYED_DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm";

	protected static final int ARRAY_INDEX_FIRST = 0;
	protected static final int ARRAY_INDEX_NOT_FOUND = -1;
	private static final int LOWEST_POSSIBLE_ID = 1;
	private static Logger logger = Logger.getLogger("Parser");

	private static final String LOG_MESSAGE_ADD_CREATED = "AddCommand Created:\nisGuiOpenCommand: %1$s\n%2$s";
	private static final String LOG_MESSAGE_EDIT_CREATED = "EditCommand Created:\nisGuiOpenCommand: %1$s"
			+ "\ntaskId: %2$s\ntaskTitle: %3$s\nisEditingTitle: %4$s\nisEditingPlace: %5$s\nisEditingStartTime: %6$s"
			+ "\nisEditingEndTime: %7$s\nisEditingProject: %8$s\nisEditingDescription: %9$s\n%10$s";
	private static final String LOG_MESSAGE_TASK_DETAILS = "Attached Task:\nID: %1$s\nTitle: %2$s\nStart: %3$s"
			+ "\nEnd: %4$s\nPlace: %5$s\nDescription: %6$s\nProject: %7$s";
	private static final String LOG_MESSAGE_NULL_TASK = "Null Task Attached.";
	private static final String LOG_MESSAGE_LIST_CREATED = "ListCommand Created:"
			+ "\nisListAll: %1$s\ndisplayDoneStates: %2$s\nkeyWordSearch: %3$s\ntitleFilter: %4$s"
			+ "\nstartTimeFilter: %5$s\nendTimeFilter: %6$s\nplaceFilter: %7$s\nprojectFilter: %8$s\ndescriptionFilter: %9$s";
	private static final String LOG_MESSAGE_DONE_CREATED = "DoneCommand Created:\ntaskId: %1$s\ntaskTitle: %2$s\nisDone: %3$s";
	private static final String LOG_MESSAGE_VIEW_CREATED = "ViewCommand Created:\ntaskId: %1$s\ntaskTitle: %2$s";
	private static final String LOG_MESSAGE_DELETE_CREATED = "DeleteCommand Created:\ntaskId: %1$s\ntaskTitle: %2$s";
	private static final String LOG_MESSAGE_UNKNOWN_CREATED = "Error: Unsupported Command Created";
	private static final String LOG_MESSAGE_HELP_CREATED = "HelpCommand created";
	private static final String LOG_MESSAGE_EXIT_CREATED = "ExitCommand created";
	private static final String LOG_MESSAGE_REDO_CREATED = "RedoCommand created";
	private static final String LOG_MESSAGE_UNDO_CREATED = "UndoCommand created";
	private static final String LOG_MESSAGE_SEARCHBOX_CREATED = "SearchboxCommand created";

	public static Command parseCommand(String rawInput)
			throws InvalidInputException {
		String userInput = trimInput(rawInput);
		generalRuleCheck(userInput);
		Command command = createCommandBasedOnType(userInput);

		logger.log(Level.INFO,
				"Command Successfully created, printing details to Log!");
		printToLog(command);
		return command;
	}

	public static Date parseTime(String timeString, boolean isStartTime)
			throws InvalidInputException {

		return DateTimeParser.parse(timeString, isStartTime);

	}

	protected static String obtainFirstWordInString(String stringInput) {
		return stringInput.split(STRING_WHITESPACE, 2)[0];
	}

	protected static boolean isTaggedAsProject(String stringToCheck) {
		if (isEmptyString(stringToCheck)) {
			return false;
		}
		if (stringToCheck.startsWith(Character.toString(TAG_PROJECT))) {
			return true;
		} else {
			return false;
		}
	}

	protected static void checkForEmptyValue(String value, String tag)
			throws InvalidInputException {

		if (value == null || isEmptyString(value)) {
			throw new InvalidInputException(String.format(
					MESSAGE_MISSING_INPUT_AFTER_TAG, tag));
		}

	}

	protected static boolean isEmptyString(String stringToCheck) {
		if (stringToCheck == null) {
			return true;
		}

		return stringToCheck.trim().equals("");
	}

	protected static boolean isSingleWord(String stringInput) {
		String trimmedString = stringInput.trim();
		return (!trimmedString.contains(STRING_WHITESPACE));
	}

	protected static ArrayList<String> getWordsInCommandDetails(String userInput) {
		ArrayList<String> wordArrayList = new ArrayList<String>();
		String[] splitInput = userInput.split(STRING_WHITESPACE, 2);
		if (splitInput.length > 1) {
			wordArrayList = getSetofWords(splitInput[1]);
		}

		return wordArrayList;
	}

	protected static ArrayList<String> getSetofWords(String userInput) {
		String[] wordArray = userInput.trim().split(STRING_WHITESPACE);
		ArrayList<String> wordArrayList = new ArrayList<String>();
		for (String word : wordArray) {
			wordArrayList.add(word);
		}
		return wordArrayList;
	}

	// This function is a follow up for getWordsInCommandDetails
	// getWordsInCommandDetails removes the first word in command
	// thus if the resulting array is empty, there are no details
	protected static void checkForMissingDetails(
			ArrayList<String> arrayListToCheck) throws InvalidInputException {
		if (arrayListToCheck.isEmpty()) {
			throw new InvalidInputException(MESSAGE_MISSING_DETAILS);
		}

	}

	protected static String removeFirstWordFromArray(
			ArrayList<String> unparsedWords) {
		assert (unparsedWords != null);
		String word = "";
		while (isEmptyString(word) && !unparsedWords.isEmpty()) {
			word = unparsedWords.remove(ARRAY_INDEX_FIRST);
		}

		if (unparsedWords.isEmpty() && isEmptyString(word)) {
			return null;
		}
		return word;
	}

	// Will also extract to the end of the array it contains no tags
	protected static String extractWordsBeforeNextTag(
			ArrayList<String> unparsedWords) {
		String extractedWords = "";

		while (!unparsedWords.isEmpty()) {
			String currentWord = unparsedWords.get(0);
			if (isNotTag(currentWord)) {
				unparsedWords.remove(0);
				extractedWords += currentWord + " ";
			} else {
				return extractedWords.trim();
			}
		}

		return extractedWords.trim();
	}

	protected static void checkForRepeatingTags(ArrayList<String> unparsedWords)
			throws InvalidInputException {

		ArrayList<String> repeatingTags = obtainRepeatingTags(unparsedWords);

		if (!repeatingTags.isEmpty()) {

			String exceptionMessage = formRepeatingTagMessage(repeatingTags);
			throw new InvalidInputException(exceptionMessage);
		}
	}

	protected static void checkForUnusableStringTags(
			ArrayList<String> unusableTags, ArrayList<String> unparsedWords,
			String commandType) throws InvalidInputException {
		String exceptionMessage = "";
		boolean isContainingUnusableTag = false;

		for (String word : unparsedWords) {
			for (String unusableTag : unusableTags) {
				if (unusableTag.equals(Character.toString(TAG_PROJECT))
						&& word.startsWith(Character.toString(TAG_PROJECT))) {
					exceptionMessage += unusableTag + " ";
					isContainingUnusableTag = true;
				} else if (word.equalsIgnoreCase(unusableTag)) {
					exceptionMessage += unusableTag + " ";
					isContainingUnusableTag = true;
				}
			}
		}

		if (isContainingUnusableTag) {
			throwExceptionUnusableTag(commandType, exceptionMessage);
		}

	}

	// Returns null if tag is not found
	protected static String extractTaggedClause(String tag,
			ArrayList<String> unparsedWords) throws InvalidInputException {
		int index = getIndexOfTag(tag, unparsedWords);

		if (index != ARRAY_INDEX_NOT_FOUND) {

			ArrayList<String> wordsAfterTag = new ArrayList<String>(
					unparsedWords.subList(index + 1, unparsedWords.size()));
			ArrayList<String> wordsBeforeTag = new ArrayList<String>(
					unparsedWords.subList(ARRAY_INDEX_FIRST, index));

			String value = extractWordsBeforeNextTag(wordsAfterTag);
			checkForEmptyValue(value, tag);

			combineInOriginalList(unparsedWords, wordsBeforeTag, wordsAfterTag);

			return value;
		} else {
			return null;
		}

	}

	protected static int getIndexOfTag(String tag,
			ArrayList<String> unparsedWords) {
		for (int i = 0; i < unparsedWords.size(); i++) {
			if (unparsedWords.get(i).equalsIgnoreCase(tag)) {
				return i;
			}
		}
		return ARRAY_INDEX_NOT_FOUND;
	}

	protected static void combineInOriginalList(ArrayList<String> originalList,
			ArrayList<String> firstList, ArrayList<String> secondList) {
		originalList.clear();

		originalList.addAll(firstList);
		originalList.addAll(secondList);
	}

	// This method assumes that a title has been checked for and not found
	// This is a follow-up method for extractTaggedClause using /id
	// which returns null if /id tag can't be found
	protected static void ifNullNoTaskReferenced(String id)
			throws InvalidInputException {
		if (id == null) {
			throw new InvalidInputException(MESSAGE_MISSING_TITLE_AND_ID);
		}

	}

	protected static void checkForReversedDates(Date startTime, Date endTime)
			throws InvalidInputException {
		if (!(startTime == Task.EMPTY_TIME || endTime == Task.EMPTY_TIME)) {
			if (startTime.after(endTime)) {
				throwExceptionReversedDates(startTime, endTime);
			}
		}

	}

	protected static void checkIfTag(String tag) throws InvalidInputException {
		if (isNotTag(tag)) {
			throw new InvalidInputException(String.format(
					MESSAGE_UNPRECEEDED_BY_TAG, tag));
		}
	}

	protected static void ensureNoIdReference(ArrayList<String> unparsedWords)
			throws InvalidInputException {
		for (String word : unparsedWords) {
			if (word.equalsIgnoreCase(TAG_ID)) {
				throw new InvalidInputException(
						MESSAGE_USE_TITLE_OR_ID_NOT_BOTH);
			}
		}

	}

	protected static boolean isNotTag(String word) {
		if (word.equalsIgnoreCase(TAG_DESCRIPTION)) {
			return false;
		}
		if (word.equalsIgnoreCase(TAG_START_TIME)) {
			return false;
		}
		if (word.equalsIgnoreCase(TAG_END_TIME)) {
			return false;
		}
		if (word.equalsIgnoreCase(TAG_PLACE)) {
			return false;
		}
		if (word.equalsIgnoreCase(TAG_KEYWORD)) {
			return false;
		}

		if (word.equalsIgnoreCase(TAG_ID)) {
			return false;
		}

		if (word.equalsIgnoreCase(TAG_DONE)) {
			return false;
		}

		if (word.equalsIgnoreCase(TAG_NOTDONE)) {
			return false;
		}

		if (word.startsWith(Character.toString(TAG_PROJECT))) {
			return false;
		}

		if (word.equalsIgnoreCase(TAG_TITLE)) {
			return false;
		}

		return true;
	}

	protected static Date parseTime(String timeString, String tag)
			throws InvalidInputException {
		assert (tag.equalsIgnoreCase(TAG_END_TIME) || tag
				.equalsIgnoreCase(TAG_START_TIME));

		Date time = new Date();
		if (tag.equalsIgnoreCase(TAG_END_TIME)) {
			time = DateTimeParser.parse(timeString,
					DateTimeParser.BOOLEAN_END_TIME);
		} else {
			time = DateTimeParser.parse(timeString,
					DateTimeParser.BOOLEAN_START_TIME);
		}
		return time;
	}

	// Presumes that the correct tag is on the word (i.e #ProjectName format)
	protected static String getProjectName(String projectWord)
			throws InvalidInputException {
		String project = projectWord.substring(1);
		if (isEmptyString(project)) {
			throw new InvalidInputException(String.format(
					MESSAGE_MISSING_INPUT_AFTER_TAG, TAG_PROJECT));
		}
		return project;
	}

	protected static int parseId(String idString) throws InvalidInputException {

		try {
			int taskId = Integer.parseInt(idString);
			if (taskId < LOWEST_POSSIBLE_ID) {
				throwExeceptionInvalidId(idString);
			}
			return taskId;

		} catch (NumberFormatException e) {
			throwExeceptionInvalidId(idString);

		}
		// Shouldn't reach here
		return LOWEST_POSSIBLE_ID;

	}

	private static void generalRuleCheck(String userInput)
			throws InvalidInputException {
		checkForEmptyInput(userInput);
	}

	private static String trimInput(String stringToTrim) {
		return stringToTrim.trim();
	}

	private static ArrayList<String> obtainRepeatingTags(
			ArrayList<String> unparsedWords) {
		ArrayList<String> tagsFound = new ArrayList<String>();
		ArrayList<String> repeatedTags = new ArrayList<String>();

		for (String word : unparsedWords) {
			String possibleTag = word.toLowerCase();

			if (!isNotTag(possibleTag)) {
				String tag = convertToActualTag(possibleTag);

				if (isContainingWord(tag, tagsFound)
						&& !isContainingWord(tag, repeatedTags)) {
					repeatedTags.add(tag);
				} else {
					tagsFound.add(tag);
				}
			}
		}

		return repeatedTags;
	}

	private static String formRepeatingTagMessage(
			ArrayList<String> repeatingTags) {
		assert (!repeatingTags.isEmpty());
		String repeatingTagsString = "";
		for (String repeatedTag : repeatingTags) {
			repeatingTagsString += repeatedTag + " ";
		}

		return String
				.format(MESSAGE_REPEATING_TAGS, repeatingTagsString.trim());
	}

	// Most tags are stand-alone delimeters
	// However, in project tag case, it appears at the start of the string
	// This method returns the project tag # alone if it is such the case
	// else it will just return the parameter as is
	private static String convertToActualTag(String tag) {
		if (tag.startsWith(Character.toString(TAG_PROJECT))) {
			return Character.toString(TAG_PROJECT);
		}
		return tag;
	}

	private static boolean isContainingWord(String word,
			ArrayList<String> wordArray) {
		for (String entry : wordArray) {
			if (entry.equals(word)) {
				return true;
			}
		}
		return false;
	}

	private static void checkForEmptyInput(String userInput)
			throws InvalidInputException {
		if (userInput == Task.EMPTY_STRING) {
			throwExceptionNoInput();
		}

		if (isEmptyString(userInput)) {
			throwExceptionNoInput();
		}

	}

	private static void throwExceptionNoInput() throws InvalidInputException {
		throw new InvalidInputException(MESSAGE_NO_INPUT);
	}

	private static void throwExceptionUnusableTag(String commandType,
			String exceptionMessage) throws InvalidInputException {
		throw new InvalidInputException(String.format(MESSAGE_UNUSABLE_TAG,
				exceptionMessage.trim(), commandType));
	}

	private static void throwExceptionReversedDates(Date startTime, Date endTime)
			throws InvalidInputException {
		SimpleDateFormat sdf = new SimpleDateFormat(
				DEFAULT_DISPLAYED_DATE_TIME_FORMAT);
		throw new InvalidInputException(String.format(MESSAGE_REVERSED_DATES,
				sdf.format(startTime), sdf.format(endTime)));
	}

	private static void throwExeceptionInvalidId(String idString)
			throws InvalidInputException {
		throw new InvalidInputException(String.format(MESSAGE_INVALID_TASK_ID,
				idString));
	}

	private static Command createCommandBasedOnType(String userInput)
			throws InvalidInputException {

		String commandType = obtainFirstWordInString(userInput).toLowerCase();
		Command command = new Command();
		switch (commandType) {
		case COMMAND_ADD:
			command = AddParser.parse(userInput);
			break;
		case COMMAND_DELETE:
			command = DDVParser.parse(userInput, COMMAND_DELETE);
			break;
		case COMMAND_VIEW:
			command = DDVParser.parse(userInput, COMMAND_VIEW);
			break;
		case COMMAND_EDIT:
			command = EditParser.parse(userInput);
			break;
		case COMMAND_DONE:
			command = DDVParser.parse(userInput, COMMAND_DONE);
			break;
		case COMMAND_NOTDONE:
			command = DDVParser.parse(userInput, COMMAND_NOTDONE);
			break;
		case COMMAND_LIST:
			command = ListParser.parse(userInput);
			break;
		case COMMAND_UNDO:
			command = createUndoCommand(userInput);
			break;
		case COMMAND_REDO:
			command = createRedoCommand(userInput);
			break;
		case COMMAND_SEARCHBOX:
			command = createSearchboxCommand(userInput);
			break;
		case COMMAND_EXIT:
			command = createExitCommand(userInput);
			break;
		case COMMAND_HELP:
			command = createHelpCommand(userInput);
			break;
		default:
			throw new InvalidInputException(MESSAGE_UNKNOWN_COMMAND);
		}

		return command;
	}

	private static Command createUndoCommand(String userInput)
			throws InvalidInputException {
		ensureOneWordOnly(userInput, COMMAND_UNDO);
		return new UndoCommand();
	}

	private static Command createRedoCommand(String userInput)
			throws InvalidInputException {
		ensureOneWordOnly(userInput, COMMAND_REDO);
		return new RedoCommand();
	}

	private static Command createSearchboxCommand(String userInput)
			throws InvalidInputException {
		ensureOneWordOnly(userInput, COMMAND_SEARCHBOX);
		return new SearchboxCommand();
	}

	private static Command createExitCommand(String userInput)
			throws InvalidInputException {
		ensureOneWordOnly(userInput, COMMAND_EXIT);
		return new ExitCommand();
	}

	private static Command createHelpCommand(String userInput)
			throws InvalidInputException {
		ensureOneWordOnly(userInput, COMMAND_HELP);
		return new HelpCommand();
	}

	private static void ensureOneWordOnly(String userInput, String command)
			throws InvalidInputException {
		if (getWordsInCommandDetails(userInput).size() != 0) {
			throw new InvalidInputException(String.format(
					MESSAGE_COMMAND_IS_ONE_WORD, command));
		}
	}

	private static void printToLog(Command command) {

		String logMessage = "";
		if (command instanceof AddCommand) {
			logMessage = addCommandLogMsg((AddCommand) command);
		} else if (command instanceof EditCommand) {
			logMessage = editCommandLogMsg((EditCommand) command);
		} else if (command instanceof DoneCommand) {
			logMessage = doneCommandLogMsg((DoneCommand) command);
		} else if (command instanceof DeleteCommand) {
			logMessage = deleteCommandLogMsg((DeleteCommand) command);
		} else if (command instanceof ViewCommand) {
			logMessage = viewCommandLogMsg((ViewCommand) command);
		} else if (command instanceof ListCommand) {
			logMessage = listCommandLogMsg((ListCommand) command);
		} else if (command instanceof SearchboxCommand) {
			logMessage = LOG_MESSAGE_SEARCHBOX_CREATED;
		} else if (command instanceof UndoCommand) {
			logMessage = LOG_MESSAGE_UNDO_CREATED;
		} else if (command instanceof RedoCommand) {
			logMessage = LOG_MESSAGE_REDO_CREATED;
		} else if (command instanceof ExitCommand) {
			logMessage = LOG_MESSAGE_EXIT_CREATED;
		} else if (command instanceof HelpCommand) {
			logMessage = LOG_MESSAGE_HELP_CREATED;
		} else {
			logMessage = LOG_MESSAGE_UNKNOWN_CREATED;
		}
		logger.log(Level.INFO, logMessage);
	}

	/*
	 * private static String uiCommandLogMsg(UiCommand uiCommand) { return
	 * "UiCommand: \ntype: " + uiCommand.type +"\n"; }
	 */

	private static String listCommandLogMsg(ListCommand listCommand) {

		return String.format(LOG_MESSAGE_LIST_CREATED, listCommand.isListAll,
				listCommand.displayDoneStates, listCommand.keyWordSearch,
				listCommand.titleFilter, listCommand.startTimeFilter,
				listCommand.endTimeFilter, listCommand.placeFilter,
				listCommand.projectFilter, listCommand.descriptionFilter);
	}

	private static String addCommandLogMsg(AddCommand addCommand) {

		return String.format(LOG_MESSAGE_ADD_CREATED,
				addCommand.isGuiOpenCommand, taskAttributeLog(addCommand.task));
	}

	private static String editCommandLogMsg(EditCommand editCommand) {

		return String.format(LOG_MESSAGE_EDIT_CREATED,
				editCommand.isGuiOpenCommand, editCommand.taskId,
				editCommand.taskTitle, editCommand.isEditingTitle,
				editCommand.isEditingPlace, editCommand.isEditingStartTime,
				editCommand.isEditingEndTime, editCommand.isEditingProject,
				editCommand.isEditingDescription,
				taskAttributeLog(editCommand.newTaskDetails));
	}

	private static String taskAttributeLog(Task task) {
		if (task == null) {
			return LOG_MESSAGE_NULL_TASK;
		}

		return String.format(LOG_MESSAGE_TASK_DETAILS, task.id, task.title,
				task.startTime, task.endTime, task.place, task.description,
				task.project);
	}

	private static String deleteCommandLogMsg(DeleteCommand deleteCommand) {
		return String.format(LOG_MESSAGE_DELETE_CREATED, deleteCommand.taskId,
				deleteCommand.taskTitle);
	}

	private static String viewCommandLogMsg(ViewCommand viewCommand) {

		return String.format(LOG_MESSAGE_VIEW_CREATED, viewCommand.taskId,
				viewCommand.taskTitle);
	}

	private static String doneCommandLogMsg(DoneCommand doneCommand) {
		return String.format(LOG_MESSAGE_DONE_CREATED, doneCommand.taskId,
				doneCommand.taskTitle, doneCommand.isDone);
	}
}
